/*
 * Copyright (c) 2015. For Intelligent Group.
 */

package com.intelligent.ispc.core.service.impl;

import com.google.common.collect.Lists;
import com.intelligent.ispc.common.mapper.BeanMapper;
import com.intelligent.ispc.common.utils.ApplicationException;
import com.intelligent.ispc.common.utils.ExceptionMessages;
import com.intelligent.ispc.common.utils.StringUtils;
import com.intelligent.ispc.core.dto.*;
import com.intelligent.ispc.core.entity.*;
import com.intelligent.ispc.core.repository.jpa.CPWarningRuleDao;
import com.intelligent.ispc.core.repository.jpa.ControlChartWarningRuleDao;
import com.intelligent.ispc.core.repository.mongo.TestDataDao;
import com.intelligent.ispc.core.service.SecurityService;
import com.intelligent.ispc.core.service.SpcAnalysisService;
import com.intelligent.ispc.external.rcall.dto.ControlDataDto;
import com.intelligent.ispc.external.rcall.dto.NormalDistributionDto;
import com.intelligent.ispc.external.rcall.service.AnalysisService;
import com.intelligent.ispc.utils.CheckType;
import com.intelligent.ispc.utils.Constant;
import com.intelligent.ispc.utils.PageSubList;
import com.intelligent.ispc.utils.SystemConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.*;
import java.util.*;
import java.util.concurrent.ExecutionException;

/**
 * Created by Julia.Zhou on 2016/02/28.
 */
@Service
public class SpcAnalysisServiceImpl extends BaseCacheServiceImpl implements SpcAnalysisService {
    private static Logger logger = LoggerFactory.getLogger(SpcAnalysisServiceImpl.class);


    @Autowired
    private CPWarningRuleDao cpWarningRuleDao;

    @Autowired
    private ControlChartWarningRuleDao controlChartWarningRuleDao;

    @Autowired
    private TestDataDao testDataDao;

    @Autowired
    private AnalysisService analysisService;

    @Autowired
    private SecurityService securityService;

    @Autowired
    private SystemConfig systemConfig;

    @Override
    public List<TestItemDto> searchTestItem(Map<String, Object> searchParams) {
        if (searchParams == null || searchParams.get("projectId") == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        List<TestData> testDataList = this.searchTestDataList(searchParams, null);
        logger.debug("Searching TestItem filtered, total elements:{}", testDataList.size());

        List<String> excludeIds = getCacheExcludeIds(searchParams);
        logger.debug("ExcludeIds:{}", excludeIds.size());

        List<TestItemDto> dtoList = Lists.newArrayList();
        for (TestData testData : testDataList) {

            if (excludeIds.contains(testData.getId())) {
                continue;
            }
            if (testData.getTestItems() == null || testData.getTestItems().isEmpty()) {
                continue;
            }

            TestItemDto dto = new TestItemDto();
            if (StringUtils.isNumeric(testData.getTestItems().get(0).getTestedValue())) {
                dto.setIsNumeric(true);
            }
            BeanMapper.copy(testData.getTestItems().get(0), dto);
            if (testData.getTestData() != null && testData.getTestData().get(TestData.PSA_SERIAL_NUMBER) != null) {
                dto.setSerialNumber(testData.getTestData().get(TestData.PSA_SERIAL_NUMBER).toString());
            }
            dto.setProjectId(testData.getProjectId());
            dto.setId(testData.getId());
            dto.setStopTime((Long) testData.getTestData().get("stopTime"));
            dto.setIsCheck(true);
            dtoList.add(dto);
        }

        return dtoList;
    }

    @Override
    public List<TestItemDto> searchTestItemFiltered(Map<String, Object> searchParams) {
        if (searchParams == null || searchParams.get("projectId") == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        List<TestData> testDataList = this.searchTestDataList(searchParams, null);
        logger.debug("Searching TestItem, total elements:{}", testDataList.size());

        String checkType = null;
        if (searchParams.get("checkType") != null) {
            checkType = (String) searchParams.get("checkType");
        }

        List<String> excludeIds = getCacheExcludeIds(searchParams);

        List<TestItemDto> dtoList = Lists.newArrayList();
        boolean hasExcludeId = false;
        for (TestData testData : testDataList) {
            if (testData.getTestItems() == null || testData.getTestItems().isEmpty()) {
                continue;
            }
            TestItemDto dto = new TestItemDto();
            if (StringUtils.isNumeric(testData.getTestItems().get(0).getTestedValue())) {
                dto.setIsNumeric(true);
            }
            BeanMapper.copy(testData.getTestItems().get(0), dto);
            if (testData.getTestData() != null && testData.getTestData().get(TestData.PSA_SERIAL_NUMBER) != null) {
                dto.setSerialNumber(testData.getTestData().get(TestData.PSA_SERIAL_NUMBER).toString());
            }
            dto.setProjectId(testData.getProjectId());
            dto.setId(testData.getId());
            dto.setStopTime((Long) testData.getTestData().get("stopTime"));
            dto.setIsCheck(true);

            if (!filterTestData(searchParams, dto.getSerialNumber(), dto.getTestedValue())) {
                continue;
            }
            if (excludeIds.contains(dto.getId())) {
                dto.setIsCheck(false);
                hasExcludeId = true;
            }
            dto.setHasExcludeId(hasExcludeId);

            if (StringUtils.isNotBlank(checkType)) {
                if (CheckType.CHECK.getCode().equals(checkType)) {
                    if (!dto.getIsCheck()) {
                        continue;
                    }
                } else if (CheckType.UNCHECK.getCode().equals(checkType)) {
                    if (dto.getIsCheck()) {
                        continue;
                    }
                }
            }
            dtoList.add(dto);
        }
        return dtoList;
    }

    @Override
    public Page<TestItemDto> searchTestItem(Map<String, Object> searchParams, Pageable pageable) {
        List<TestItemDto> testItemDtos = this.searchTestItemFiltered(searchParams);
        int count = testItemDtos.size();

        Page<TestItemDto> pageDto = new PageImpl<TestItemDto>(PageSubList.getPageList(testItemDtos, pageable.getPageNumber(), pageable.getPageSize(), count), pageable, count);
        return pageDto;
    }

    @Override
    public boolean saveExcludeIds(Map<String, Object> searchParams) {
        if (searchParams == null || searchParams.get("projectId") == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        Boolean isAllCheck = null;
        String includeId = null;
        String excludeId = null;

        if (searchParams.containsKey("isAllCheck") && searchParams.get("isAllCheck") != null) {
            isAllCheck = "true".equals((String) searchParams.get("isAllCheck")) ? true : false;
        }

        if (searchParams.containsKey("includeId") && searchParams.get("includeId") != null) {
            includeId = (String) searchParams.get("includeId");
        }

        if (searchParams.containsKey("excludeId") && searchParams.get("excludeId") != null) {
            excludeId = (String) searchParams.get("excludeId");
        }

        Long userId = securityService.getCurrentUser().getId();
        searchParams.put("userId", userId);

        List<String> excludeIds = getCacheExcludeIds(searchParams);
        if (isAllCheck == null) {
            if (StringUtils.isNotBlank(includeId) && excludeIds.contains(includeId)) {
                excludeIds.remove(includeId);
            }

            if (StringUtils.isNotBlank(excludeId) && !excludeIds.contains(excludeId)) {
                excludeIds.add(excludeId);
            }
        } else {
            List<String> oldResutIds = new ArrayList<>();
            List<TestItemDto> testDataList = this.searchTestItemFiltered(searchParams);
            logger.debug("Searching TestItem, total elements:{}", testDataList.size());
            for (TestItemDto testData : testDataList) {
                oldResutIds.add(testData.getId());
            }
            if (isAllCheck) {
                for (String inId : oldResutIds) {
                    if (excludeIds.contains(inId)) {
                        excludeIds.remove(inId);
                    }
                }
            } else {
                for (String exId : oldResutIds) {
                    if (!excludeIds.contains(exId)) {
                        excludeIds.add(exId);
                    }
                }
            }
        }

        Map<String, Object> searchExcludeMap = new HashMap<>();
        searchExcludeMap.put("projectId", searchParams.get("projectId"));
        searchExcludeMap.put("userId", securityService.getCurrentUser() == null ? null : securityService.getCurrentUser().getId().toString());
        testDataCacheFactory.getCacheExcludeIds().put(mapper.toJson(searchExcludeMap), mapper.toJson(excludeIds));
        return !excludeIds.isEmpty();
    }


    @Override
    public List<BaseDto> searchDistinctTestItem(Map<String, Object> searchParams) {
        if (searchParams == null || searchParams.get("projectId") == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        List<BaseDto> dtoList = Lists.newArrayList();
        List<String> results = testDataDao.findDistinctTestItems(searchParams);
        String itemName = (String) searchParams.get("itemName");

        logger.debug("Searching TestItem, total elements:{}", results.size());
        for (String testItemName : results) {
            if (itemName != null && !testItemName.contains(itemName)) {
                continue;
            }
            BaseDto dto = new BaseDto();
            dto.setValue(testItemName);
            dtoList.add(dto);
        }

        return dtoList;
    }

    @Override
    public Page<BaseDto> searchDistinctTestItem(Map<String, Object> searchParams, Pageable pageable) {
        List<BaseDto> dtoList = searchDistinctTestItem(searchParams);

        Page<BaseDto> pageDto = new PageImpl<BaseDto>(PageSubList.getPageList(dtoList, pageable.getPageNumber(), pageable.getPageSize(), dtoList.size()), pageable, dtoList.size());
        return pageDto;
    }

    @Override
    public List<StationDto> findStationByProject(String projectId) {
        if (StringUtils.isBlank(projectId)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        List<StationDto> dtoList = Lists.newArrayList();
        if (StringUtils.isBlank(projectId)) {
            return dtoList;
        }
        Map<String, Object> searchParams = new HashMap<>();
        searchParams.put("projectId", projectId);
        try {
            String strList = testDataCacheFactory.getCacheDistinctTestDatas().get(mapper.toJson(searchParams));
            List<TestStation> results = mapper.fromJson(strList, mapper.contructCollectionType(List.class, TestStation.class));

            logger.debug("Searching Station, total elements:{}", results.size());
            if (results != null && !results.isEmpty()) {
                for (TestStation testStation : results) {
                    if (StringUtils.isBlank(testStation.getStationId())) {
                        continue;
                    }
                    StationDto stationDto = new StationDto();
                    BeanMapper.copy(testStation, stationDto);
                    dtoList.add(stationDto);
                }
            }
        } catch (ExecutionException e) {
            logger.debug("Searching Station fail, error message:{}", e.getMessage());
            throw new ApplicationException(ExceptionMessages.EXCEPTION_SYSTEM);
        }

        return dtoList;
    }

    @Override
    public List<BaseDto> searchDistinctSlot(Map<String, Object> searchParams) {
        if (searchParams == null || searchParams.get("projectId") == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        List<BaseDto> dtoList = Lists.newArrayList();
        String strList = null;
        try {
            strList = testDataCacheFactory.getCacheDistinctTestDatas().get(mapper.toJson(searchParams));
            List<TestStation> results = mapper.fromJson(strList, mapper.contructCollectionType(List.class, TestStation.class));
            if (results != null && !results.isEmpty()) {
                for (TestStation testStation : results) {
                    List<String> slotNoList = testStation.getSlotNos();
                    logger.debug("Searching slotNo, total elements:{}", slotNoList.size());
                    Collections.sort(slotNoList);
                    for (String slotNo : slotNoList) {
                        if (StringUtils.isNotBlank(slotNo)) {
                            BaseDto dto = new BaseDto();
                            dto.setValue(slotNo);
                            dtoList.add(dto);
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.debug("Searching slotNo fail, error message:{}", e.getMessage());
            throw new ApplicationException(ExceptionMessages.EXCEPTION_SYSTEM);
        }


        return dtoList;
    }

    @Override
    public List<String> searchDistinctSN(Map<String, Object> searchParams) {
        if (searchParams == null || searchParams.get("projectId") == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        List<String> snList = Lists.newArrayList();
        try {
//            strList = testDataCacheFactory.getCacheDistinctTestDatas().get(mapper.toJson(searchParams));
//            List<TestStation> results = mapper.fromJson(strList, mapper.contructCollectionType(List.class, TestStation.class));

            Map<String, Object> filters = mapper.fromJson(mapper.toJson(searchParams), mapper.contructMapType(Map.class, String.class, Object.class));

            snList = testDataDao.findDistinctTestDatas(filters, "testData.serialNumber");

        } catch (Exception e) {
            logger.debug("Searching slotNo fail, error message:{}", e.getMessage());
            throw new ApplicationException(ExceptionMessages.EXCEPTION_SYSTEM);
        }

        return snList;
    }

    @Override
    public List<CPWarningRuleDto> searchCPWarningRuleByProject(String projectId) {
        if (StringUtils.isBlank(projectId)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        Specification<CPWarningRule> spec = (Root<CPWarningRule> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
            Join<CPWarningRule, Project> projectJoin = root.join(root.getModel().getSingularAttribute("project", Project.class), JoinType.LEFT);
            List<Predicate> predicates = Lists.newArrayList();
            Predicate p = cb.equal(projectJoin.get("id").as(String.class), projectId);
            predicates.add(p);

            if (!predicates.isEmpty()) {
                query.where(cb.and(predicates.toArray(new Predicate[predicates.size()])));
            }

            return query.getRestriction();
        };

        List<CPWarningRule> entityList = cpWarningRuleDao.findAll(spec);
        logger.debug("Searching CPWarningRule, total elements:{}", entityList.size());
        List<CPWarningRuleDto> dtoList = Lists.newArrayList();

        for (CPWarningRule entity : entityList) {
            CPWarningRuleDto dto = new CPWarningRuleDto();
            BeanMapper.copy(entity, dto);
            dtoList.add(dto);
        }

        return dtoList;
    }

    @Override
    public List<ControlChartWarningRuleDto> searchCCWarningRuleByProject(String projectId) {
        if (StringUtils.isBlank(projectId)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        Specification<ControlChartWarningRule> spec = (Root<ControlChartWarningRule> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
            Join<ControlChartWarningRule, Project> projectJoin = root.join(root.getModel().getSingularAttribute("project", Project.class), JoinType.LEFT);
            List<Predicate> predicates = Lists.newArrayList();
            Predicate p = cb.equal(projectJoin.get("id").as(String.class), projectId);
            predicates.add(p);

            if (!predicates.isEmpty()) {
                query.where(cb.and(predicates.toArray(new Predicate[predicates.size()])));
            }

            return query.getRestriction();
        };

        List<ControlChartWarningRule> entityList = controlChartWarningRuleDao.findAll(spec);
        logger.debug("Searching CPWarningRule, total elements:{}", entityList.size());
        List<ControlChartWarningRuleDto> dtoList = Lists.newArrayList();

        for (ControlChartWarningRule entity : entityList) {
            ControlChartWarningRuleDto dto = new ControlChartWarningRuleDto();
            BeanMapper.copy(entity, dto);
            dtoList.add(dto);
        }

        return dtoList;
    }

    @Override
    public NormalDistributionDto analyseNormalDistribution(Map<String, Object> searchParams) {
        String usl = null;
        String lsl = null;
        if (searchParams.containsKey("usl")) {
            usl = (String) searchParams.get("usl");
            searchParams.remove("usl");
        }

        if (searchParams.containsKey("lsl")) {
            lsl = (String) searchParams.get("lsl");
            searchParams.remove("lsl");
        }

        List<TestItemDto> testItemDtos = searchTestItem(searchParams);
        if (testItemDtos == null || testItemDtos.isEmpty()) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_SPC_NO_DATA);
        }

        List<CPWarningRuleDto> cpWarningRuleDtos = Lists.newArrayList();
        if (searchParams.containsKey("projectId")) {
            String projectId = (String) searchParams.get("projectId");
            cpWarningRuleDtos = searchCPWarningRuleByProject(projectId);
        }

        if (usl != null) {
            searchParams.put("usl", usl);
        }

        if (lsl != null) {
            searchParams.put("lsl", lsl);
        }

        return analysisService.analyseNormalDistribution(searchParams, testItemDtos, cpWarningRuleDtos);
    }

    @Override
    public ControlDataDto analyseControlChart(Map<String, Object> searchParams) {
        String type = null;
        if (searchParams.get(Constant.ANALYSE_TYPE) != null) {
            type = searchParams.get(Constant.ANALYSE_TYPE).toString();
            searchParams.remove(Constant.ANALYSE_TYPE);
        }

        List<TestItemDto> testItemDtos = searchTestItem(searchParams);
//        List<TestItemDto> realItemDtos = Lists.newArrayList();
        int totalNumber = testItemDtos.size();

        searchParams.put(Constant.ANALYSE_TYPE, type);
        List<ControlChartWarningRuleDto> ruleList = Lists.newArrayList();
        if (searchParams.containsKey("projectId")) {
            String projectId = (String) searchParams.get("projectId");
            ruleList = searchCCWarningRuleByProject(projectId);
        }

        ControlDataDto controlDataDto = analysisService.analyseControlChart(searchParams, testItemDtos, ruleList);
        controlDataDto.setTotalNumber(testItemDtos.size());

        if (searchParams.containsKey("start") && searchParams.containsKey("end")) {
            int start = new Integer(searchParams.get("start").toString());
            int end = new Integer(searchParams.get("end").toString());
            if (end > totalNumber) {
                end = totalNumber - 1;
            }
            double[] datas = controlDataDto.getData();
            if (end > datas.length) {
                end = datas.length;
            }

            double[] newData = Arrays.copyOfRange(datas, start, end);
            controlDataDto.setData(newData);

            if (controlDataDto.getTimeStop() != null) {
                Long[] index = controlDataDto.getTimeStop();
                Long[] newIndex = Arrays.copyOfRange(index, start, end);
                controlDataDto.setTimeStop(newIndex);
            }
        }


        return controlDataDto;
    }

    @Override
    public void deleteTestData(String uuid, String projectId) {
        logger.info("Delete test file: project={}, uuid={}", projectId, uuid);
        if (StringUtils.isBlank(projectId)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        try {
            Map<String, Object> searchMap = new HashMap<>();
            searchMap.put("testFileUUID", uuid);
            searchMap.put("isDelete", true);
            searchMap.put("projectId", projectId.toString());

            testDataDao.findAllAndRemove(searchMap);
        } catch (Exception e) {
            logger.info("Delete test data fail: project={}, uuid={}, error={}", projectId, uuid, e.getMessage());
        }
    }

    @Override
    public void invalidateCacheExcludeIds(Map<String, Object> searchParams) {
        if (searchParams == null || searchParams.get("projectId") == null || securityService.getCurrentUser() == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        String projectId = (String) searchParams.get("projectId");
        String userId = securityService.getCurrentUser().getId().toString();
        logger.debug("Checking params: projectId={}, userId={}", projectId, userId);

        Map<String, Object> searchExcludeMap = new HashMap<>();
        searchExcludeMap.put("projectId", searchParams.get("projectId"));
        searchExcludeMap.put("userId", userId);
        List<String> excludeIdList = Lists.newArrayList();
        Set<String> keys = testDataCacheFactory.getCacheExcludeIds().asMap().keySet();
        for (String key : keys) {
            Map<String, Object> searchParamsMap = mapper.fromJson(key, mapper.contructMapType(Map.class, String.class, Object.class));
            String projectIdCache = searchParamsMap.get("projectId") == null ? null : searchParamsMap.get("projectId").toString();
            String userIdCache = searchParamsMap.get("userId") == null ? null : searchParamsMap.get("userId").toString();
            if (projectId.equals(projectIdCache) && userId.equals(userIdCache)) {
                excludeIdList.add(key);
            }
        }
        testDataCacheFactory.getCacheExcludeIds().invalidateAll(excludeIdList);
    }

    private List<String> getCacheExcludeIds(Map<String, Object> searchParams) {
        List<String> excludeIds = new ArrayList<>();
        Map<String, Object> searchExcludeMap = new HashMap<>();
        searchExcludeMap.put("projectId", searchParams.get("projectId"));
        if (searchParams.get("userId") != null) {
            searchExcludeMap.put("userId", String.valueOf((Long) searchParams.get("userId")));
        } else {
            searchExcludeMap.put("userId", securityService.getCurrentUser() == null ? null : securityService.getCurrentUser().getId().toString());
        }

        try {
            excludeIds = getExcludeIds(searchExcludeMap);
        } catch (Exception e) {
            logger.debug("Searching exclude ids fail, error message:{}", e.getMessage());
            throw new ApplicationException(ExceptionMessages.EXCEPTION_SYSTEM);
        }
        return excludeIds;
    }

    private boolean filterTestData(Map<String, Object> searchParams, String sn, String testValue) {
        boolean isHas = true;
        String filterValue = null;
        if (searchParams.get("filterValue") != null) {
            filterValue = (String) searchParams.get("filterValue");
        }
        if (StringUtils.isNotBlank(filterValue) && !sn.toLowerCase().contains(filterValue) && (StringUtils.isBlank(testValue) || !testValue.toLowerCase().contains(filterValue))) {
            isHas = false;
        }
        return isHas;
    }
}
